import os  # 导入操作系统模块，用于路径处理
from urllib.parse import unquote

import pymysql  # 导入PyMySQL库，用于MySQL数据库操作
from scrapy.pipelines.images import ImagesPipeline  # 导入Scrapy的图片管道
from scrapy.pipelines.files import FilesPipeline  # 导入Scrapy的文件管道
from twisted.enterprise import adbapi  # 导入Twisted的异步数据库连接池
from scrapy.exceptions import DropItem  # 导入Scrapy的DropItem异常
import scrapy  # 导入Scrapy框架

from painoScore import settings


# 1. 图片下载管道（基于ImagesPipeline）
class ImageDownloadPipeline(ImagesPipeline):
    """
    图片下载管道类 这是一个继承自Scrapy的ImagesPipeline的图片下载管道类，用于处理图片的下载和存储功能。
    参数：
    store_uri：存储图片的URI地址
    download_func：可选的下载函数，用于自定义下载行为
    settings：可选的设置参数，用于配置管道行为
    """

    # 正确初始化父类（传递所有参数）
    def __init__(self, store_uri, download_func=None, settings=None):
        # 调用ImagesPipeline父类的完整初始化
        super().__init__(store_uri, download_func, settings)

    def get_media_requests(self, item, info):
        '''
        概述：
        该函数用于处理媒体请求，特别是图片下载请求，它为每个图片URL生成一个Scrapy请求，
        并携带索引信息以确保下载的图片与原始列表中的元素正确对应。
        参数：
        item：包含要处理的数据项，特别是包含image_urls字段，该字段是一个图片URL列表
        info：Scrapy的spider信息对象，用于获取spider实例
        返回值：
        返回一个生成器(yield)，生成多个scrapy.Request对象，每个请求对应一个图片URL，并携带原始item和该图片在列表中的索引位置作为meta信息
        '''
        self.spider = info.spider  # 将当前爬虫实例保存到self.spider中
        # 传递索引，确保下载的图片与列表元素对应
        if item.get('image_urls'):
            for idx, img_url in enumerate(item['image_urls']):
                yield scrapy.Request(
                    img_url,
                    meta={'item': item, 'index': idx},  # 携带索引信息，用于后续处理
                    dont_filter=True  # 避免因重复URL被过滤
                )

    def file_path(self, request, response=None, info=None):
        '''
        概述：该函数用于生成图片的本地存储路径，它根据索引信息生成文件夹名称，并使用图片URL的文件名作为图片的存储名称。
        '''
        item = request.meta['item']  # 从请求的meta中获取item对象
        idx = request.meta['index']  # 从请求的meta中获取当前元素的索引
        title = item['title'].replace('/', '_') if item['title'] else f'未知标题_{idx}'
        return f'images/{title}/{os.path.basename(request.url)}'  # 返回图片的保存路径，格式为：images/文件夹名/图片文件名

    def item_completed(self, results, item, info):
        '''
        概述：该函数用于处理图片下载完成后的结果，它将下载的图片数据存储到item的image_data字段中。
        '''
        item['image_data'] = [None] * len(item.get('image_urls', []))  # 初始化图片数据列表，长度与图片URL列表相同，初始值为None
        for (ok, result), idx in zip(results, range(len(results))):  # 遍历下载结果和索引
            if ok:  # 如果下载成功且索引在有效范围内
                # 优先尝试从'response'获取数据，若不存在则从本地文件读取
                if 'response' in result:
                    item['image_data'][idx] = result['response'].body
                elif 'path' in result:
                    # 从本地文件路径读取数据（处理缓存或本地文件情况）
                    try:
                        image_path = settings.IMAGES_STORE + '/' + result['path']
                        with open(image_path, 'rb') as f:
                            item['image_data'][idx] = f.read()
                    except Exception as e:
                        self.spider.logger.error(f"读取本地图片失败（索引{idx}）: {str(e)}")
                else:
                    # 既没有response也没有path，标记为失败
                    ok = False
            if not ok:
                self.spider.logger.warning(
                    f"图片处理失败（索引{idx}）: {item['title'][idx] if (item['title'] and idx < len(item['title'])) else '未知标题'}")
        return item


# 2. PDF下载管道（基于FilesPipeline）
class PdfDownloadPipeline(FilesPipeline):
    # 正确初始化父类（传递所有参数）
    def __init__(self, store_uri, download_func=None, settings=None):
        # 调用ImagesPipeline父类的完整初始化
        super().__init__(store_uri, download_func, settings)

    def get_media_requests(self, item, info):
        self.spider = info.spider  # 将当前爬虫实例保存到self.spider中
        # 传递索引，确保下载的图片与列表元素对应
        if item.get('file_urls'):
            for idx, file_url in enumerate(item['file_urls']):
                yield scrapy.Request(
                    file_url,
                    meta={'item': item, 'index': idx}  # 携带索引信息，用于后续处理
                )

    def file_path(self, request, response=None, info=None):
        item = request.meta['item']  # 从请求的meta中获取item对象
        idx = request.meta['index']  # 从请求的meta中获取当前元素的索引
        title = item['title'].replace('/', '_') if item['title'] else f'未知标题_{idx}'
        # 对 URL 编码‌的字符串进行解码
        basename = unquote(str({os.path.basename(request.url)})).replace("{'", "").replace("'}", "")
        return f'images/{title}/{basename}.pdf'  # 返回图片的保存路径，格式为：images/文件夹名/图片文件名

    def item_completed(self, results, item, info):
        item['file_data'] = [None] * len(item.get('file_urls', []))  # 初始化图片数据列表，长度与图片URL列表相同，初始值为None
        for (ok, result), idx in zip(results, range(len(results))):  # 遍历下载结果和索引
            if ok and idx < len(item['file_data']):  # 如果下载成功且索引在有效范围内
                # 优先尝试从'response'获取数据，若不存在则从本地文件读取
                if 'response' in result:
                    item['file_data'][idx] = result['response'].body
                    break
                elif 'path' in result:
                    # 从本地文件路径读取数据（处理缓存或本地文件情况）
                    try:
                        file_path = settings.FILES_STORE + '/' + result['path']
                        with open(file_path, 'rb') as f:
                            item['file_data'][idx] = f.read()
                    except Exception as e:
                        self.spider.logger.error(f"读取本地PDF失败（索引{idx}）: {str(e)}")
                else:
                    # 既没有response也没有path，标记为失败
                    ok = False
            if not ok:
                self.spider.logger.warning(
                    f"PDF处理失败（索引{idx}）: {item['title'] if (item['title'] and idx < len(item['title'])) else '未知标题'}")
        return item


# 3. 数据库存储管道
class DatabasePipeline:
    def __init__(self, dbpool):
        self.dbpool = dbpool  # 异步数据库连接池

    @classmethod  # 使用类装饰器，表示这是一个类方法
    def from_crawler(cls, crawler):
        """从配置中初始化数据库连接池"""
        db_params = {  # 数据库连接参数配置字典
            'host': crawler.settings.get('MYSQL_HOST'),  # MySQL服务器地址
            'user': crawler.settings.get('MYSQL_USER'),  # MySQL用户名
            'password': crawler.settings.get('MYSQL_PASSWORD'),  # MySQL密码
            'database': crawler.settings.get('MYSQL_DATABASE'),  # MySQL数据库名
            'charset': 'utf8mb4',  # 字符集设置为utf8mb4，支持完整的Unicode字符
            'cursorclass': pymysql.cursors.DictCursor,  # 使用字典游标，查询结果以字典形式返回
        }
        dbpool = adbapi.ConnectionPool('pymysql', **db_params)  # 创建异步数据库连接池，使用pymysql作为连接库
        return cls(dbpool)

    def process_item(self, item, spider):
        """异步插入数据库"""
        # 使用数据库连接池执行异步交互操作，将item插入数据库
        # self.dbpool 是数据库连接池对象
        # runInteraction 方法执行给定的回调函数，并在其中处理数据库操作
        # self.insert_db 是实际的数据库插入函数
        # item 是要插入的数据项
        query = self.dbpool.runInteraction(self.insert_db, item, spider)
        # 添加错误回调函数，如果插入过程中发生错误，将调用 handle_error 方法
        # handle_error 方法接收 item 和 spider 参数，用于错误处理
        query.addErrback(self.handle_error, item, spider)
        # 返回处理后的 item，传递给后续的 pipeline 处理
        return item

    def insert_db(self, cursor, item, spider):
        """执行数据库插入"""
        sql = """
        INSERT INTO piano_scores (
            title, score_num, singer, click_count, description, image_data, image_url, file_data, file_url
        ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
        """
        cursor.execute(sql, (
            item.get('title', ''),
            item.get('score_num', ''),
            item.get('singer', ''),
            item.get('click_count', ''),
            item.get('description', ''),
            item.get('image_data', None),
            item.get('image_url', ''),
            item.get('file_data', None),
            item.get('file_url', '')
        ))
        spider.logger.info(f"数据存储成功: {item['title']}")

    def handle_error(self, failure, item, spider):
        """处理数据库错误"""
        spider.logger.error(f"数据库错误: {failure} | 标题: {item['title']}")
        # 下载失败时记录警告日志，不中断整个流程。
        # 数据库插入错误时丢弃当前 item，继续处理后续数据。
        raise DropItem(f"数据存储失败: {item['title']}")

    def close_spider(self, spider):
        """关闭数据库连接池"""
        self.dbpool.close()
        spider.logger.info("数据库连接已关闭")